/*
 * Compresor.cpp
 *      Author: Klow
 */

#include <stdlib.h>
#include <stdio.h>

#include "Compresor.h"
#include "Aritmetico.h"
#include "Buffers/Buffer_entrada.h"
#include "Buffers/Buffer_salida.h"
#include "Logger_Compresor.h"


Compresor::Compresor(ManejadorContexto* manejador_contexto){

	//inicializo el manejador de contextos con el archivo pasado por parametro
	this->manejador = manejador_contexto;
}

Compresor::Resultado_compresor Compresor::comprimir(int orden,string archivo_origen){

	FILE * file_origen = NULL;
	FILE * file_destino = NULL;

	//Intento abrir los archivos, si no puedo retorno 1 (fracaso al comprimir)
	if(abrir_archivos_comprimir(archivo_origen,&file_origen,&file_destino) != 0) return OPERACION_FALLIDA_NO_ARCHIVO;
	//asigno el archivo de escritura al aritmetico
	aritmetico.asignarArchivo(file_destino);
	//creo el contexto cero
	string ctx = "";
	Contexto contexto_cero(ctx);
	manejador->altaContexto(&contexto_cero);

	Logger_Compresor logger(archivo_origen+".log");
	TLetraExt caracter_leido = 0;  //caracter leido del archivo
	TLetraExt * caracter_contexto = NULL; //puntero a caracter leido del contexto
	string cadena_contexto; //cadena donde se guardara el contexto
	string cadena_contexto_actual;//cadena donde se guarda el ctx actual
	int orden_actual; //orden que ira manejando el compresor
	Contexto* contexto_actual = NULL; //contexto actual
	Contexto* contexto_copia = NULL;  //copia del contexto actual

	Buffer_entrada* buffer = new Buffer_entrada(file_origen,TAM_BUFFER);//buffer para leer del archivo

	this->inicializar_metada(file_destino,orden,archivo_origen.c_str());

	while(caracter_leido!= FIN_DE_ARCHIVO){ //comprime hasta que encuentre el ultimo caracter


		caracter_leido = buffer->get_caracter();

		exclusion.limpiarExclusion();
		orden_actual = cadena_contexto_actual.length();

		do {
			if(orden_actual == -1){
				contexto_copia = NULL;  //representa a ctx-1
				caracter_contexto = new TLetraExt (caracter_leido);
			    logger.loggear_compression_caracter(contexto_copia,*caracter_contexto);
			}else{
				contexto_actual = manejador->buscarContexto(cadena_contexto_actual);//busco el contexto
				if(contexto_actual != NULL){//si el contexto existe
					contexto_copia = crear_contexto_copia(contexto_actual);//creo una copia con la exclusion ya realizada
					caracter_contexto = contexto_copia->devolverCaracter(caracter_leido);//buesco el caract en el ctx

					//actualizo el contexto actual (original)
					if(caracter_contexto == NULL ){
						if(contexto_actual->getFrecuenciaTotal()!=1){
							contexto_actual->actualizarEsc();
							manejador->modificarContexto(contexto_actual);
							logger.loggear_compression_escape(contexto_actual);
						}
					}
					else{
						this->estadisticas[orden_actual] = this->estadisticas[orden_actual] + 1;
						logger.loggear_compression_caracter(contexto_copia,*caracter_contexto);
					}
				}
				else{ //si el ctx no existe, lo creo genero copia y lo guardo
					contexto_actual = new Contexto(cadena_contexto_actual);
					contexto_copia = crear_contexto_copia(contexto_actual);
					manejador->altaContexto(contexto_actual);
					caracter_contexto = NULL;
					logger.loggear_creacion_ctx(contexto_actual);
					logger.loggear_compression_escape(contexto_actual);

				}

				if(contexto_actual){ //si no es el ctx-1 actualizo las excluciones y decremento el orden

					//actualizo la cadena contexto
					if(cadena_contexto_actual.length() > 0)
						cadena_contexto_actual = cadena_contexto_actual.substr(1,cadena_contexto_actual.length()-1);
					//actualizo el contexto
					contexto_actual->actualizar(caracter_leido);
					manejador->modificarContexto(contexto_actual);
					orden_actual--;
				}
			}

			this->aritmetico.comprimir(contexto_copia,caracter_contexto);

			//libero recursos utilizados
			if(contexto_copia){
				delete contexto_copia;
				contexto_copia= NULL;
			}
			if(contexto_actual){
				delete contexto_actual;
				contexto_actual = NULL;
			}

		} while (caracter_contexto == NULL);//repite hasta encotrar el caracter en el contexto

		//agrego el caracter al contexto y en caso de ser orden max recorto la cadena
		if (caracter_leido != FIN_DE_ARCHIVO) {
			cadena_contexto+= (TLetra)caracter_leido;
		}

		if((int)cadena_contexto.length() > orden){
			cadena_contexto = cadena_contexto.substr(1,orden);
		}
		//Reinicio la cadena actual
		cadena_contexto_actual = cadena_contexto;

		//libero recursos del carcater_contexto
		delete caracter_contexto;
		caracter_contexto = NULL;
		//}
	}

	//Me aseguro de estar en el final para poder calcular el tamanio del archivo original
	fseek(file_origen,0,SEEK_END);

	this->persistir_metadata(file_destino,orden,archivo_origen.c_str(),ftell(file_origen));
	//cierro los archivos utilizados

	fclose(file_origen);
	fclose(file_destino);
	delete buffer;

	return OPERACION_EXITOSA;//retorno compresion exitosa
}

void Compresor::actualizarContextos(int orden_actual, unsigned int orden, string  cadena_contexto, TLetraExt caracter_leido)
{
	unsigned int numContexto;
	int offset;

	numContexto = orden_actual + 1 ;

    // si ya actualizo todos los ctxs
    bool actualizado = false;

    while(numContexto <= orden && !actualizado){ //actualizo los contextos hasta el max

    	offset=cadena_contexto.length()-numContexto;
    	string aux_cadena = cadena_contexto.substr(offset, numContexto);
       	Contexto *aux = manejador->buscarContexto(aux_cadena);
        if(aux){
            aux->actualizar(caracter_leido);
            manejador->modificarContexto(aux);
        }
        delete aux;
        //si ya se recorrio toda la cadena termino
        if(numContexto == cadena_contexto.length()) actualizado = true;
        numContexto++;
    }
}

Compresor::Resultado_compresor Compresor::descomprimir(string archivo_origen){


	FILE * file_origen = NULL;
	FILE * file_destino = NULL;

	unsigned int orden = 0;
	char * nombreArchivo;
	//Intento abrir los archivos, si no puedo retorno 1 (fracaso al comprimir)
	if (abrir_archivos_descomprimir(archivo_origen, &file_origen,& file_destino,&orden,&nombreArchivo) != 0)
		return OPERACION_FALLIDA_NO_ARCHIVO;

	//para saltear el metadata
	this->inicializar_metada(file_origen,orden,nombreArchivo);

	//asigno el archivo de escritura al aritmetico
	aritmetico.asignarArchivo(file_origen);
	aritmetico.inicializarNumero();

	Logger_Compresor logger(archivo_origen + ".log");
	int orden_actual; //orden que ira manejando el compresor
	TLetraExt * caracter_leido = NULL; //puntero a caracter descomprimido
	string cadena_contexto; //cadena donde se guardara el contexto
	string cadena_contexto_actual; //cadena con el contexto actual
	Contexto* contexto_actual = NULL; //contexto actual
	Contexto* contexto_copia = NULL; //copia del contexto actual

	Buffer_salida * buffer = new Buffer_salida(file_destino,TAM_BUFFER);

	do { //descomprime hasta que encuentre el ultimo caracter

		if(caracter_leido){ //Seteo el caracter leido a NULL
			delete caracter_leido;
			caracter_leido = NULL;
		}
		exclusion.limpiarExclusion();
		orden_actual = cadena_contexto_actual.length();

		do {
			if (orden_actual == -1) {
				contexto_copia = NULL; //representa a ctx-1

			} else {
				//Busco el contexto actual
				contexto_actual = manejador->buscarContexto(cadena_contexto_actual);

				if (contexto_actual != NULL) { //si existe el contexto actual creo la copia
					contexto_copia = crear_contexto_copia(contexto_actual);//creo una copia con la exclusion ya realizada

				} else {
					//Como no existe el contexto lo creo y creo una copia
					contexto_actual = new Contexto(cadena_contexto_actual);
					contexto_copia = crear_contexto_copia(contexto_actual);
					manejador->altaContexto(contexto_actual);
					logger.loggear_creacion_ctx(contexto_actual);

				}
			}

			if (contexto_actual) { //si NO es el ctx-1 actualizo
				//actualizo exclusiones
				excluir_caracteres(contexto_actual);
				//decremento la cadena del contexto
				if(cadena_contexto_actual.length() > 0)
					cadena_contexto_actual = cadena_contexto_actual.substr(1,cadena_contexto_actual.length()-1);
				//decremento el orden actual
				orden_actual--;

			}
			//Descomprimo el caracter
			caracter_leido = this->aritmetico.descomprimir(contexto_copia);

			//Actualizo el contexto actual viendo el caracter leido
			if (caracter_leido == NULL){
				//Se actualizan la cantidad de escapes si es que no es el unico caracter presente
				if(contexto_actual->getFrecuenciaTotal()!=1){
					contexto_actual->actualizarEsc();
					manejador->modificarContexto(contexto_actual);
					logger.loggear_descompression_escape(contexto_actual);
				}
			} else if(*caracter_leido != FIN_DE_ARCHIVO){
				//Escribo el carcater en el archivo
				buffer->agregarCaracter((char)*caracter_leido);
				logger.loggear_descompression_caracter(contexto_actual,*caracter_leido);
			}

			//libero recursos utilizados
			if(contexto_copia){
				delete contexto_copia;
				contexto_copia= NULL;
			}
			if(contexto_actual){
				delete contexto_actual;
				contexto_actual = NULL;
			}

		} while (caracter_leido == NULL);

		//actualizo los contextos anteriores
		actualizarContextos(orden_actual, orden, cadena_contexto, *caracter_leido);

		//agrego el caracter al contexto y en caso de ser orden max recorto la cadena
		char * cr = new char[2];
		cr[0]= *caracter_leido;
		cr[1]='\0';
		cadena_contexto.append(cr);
		delete[](cr);

		if((cadena_contexto.length()) > orden) {
				//achivo la cadena para dejarla del tamanio del orden max
				cadena_contexto = cadena_contexto.substr(1, orden);
			}
			//Reinicio la cadena actual
			cadena_contexto_actual = cadena_contexto;

	}while (*caracter_leido != FIN_DE_ARCHIVO);

	//libero recursos
	delete caracter_leido;
	delete buffer;
	//cierro los archivos utilizados
	fclose(file_origen);
	fclose(file_destino);
	//
	delete[](nombreArchivo);

	return OPERACION_EXITOSA;//retorno compresion exitosa
}

int Compresor::imprimir_estadisticas(string archivo_origen){

	/*A imprimir
	 * Orden del archivo comprimido.
	 * Nombre original
	 * Tamano Original / Tamaño Comprimido
	 * Tamanio metada (que se guarda)
	 * Estadisticas
	 */
	//abro el archivo en modo binario
	FILE* file = fopen (archivo_origen.c_str(),"r");
	//si no puedo abrir el archivo cierro el archivo origen y retorno 1(fracaso)
	if(file == NULL){
		return Compresor::OPERACION_FALLIDA_NO_ARCHIVO;
	}

	unsigned int orden;
	char * nombreArchivo;
	unsigned int tamanioOriginal=0;
	unsigned int tamanioMetadataTotal;
	unsigned int tamanioComprimido;

	unsigned int actual = 0;
	unsigned int longitud = 0;
	unsigned int cantAciertos;

	this->obtenerInformacionBasica(file,&orden,&nombreArchivo);

	//Tamanio del metadata restante. Tamañoo Original + Estadisticas
	unsigned int tamanioMetadata = sizeof(unsigned int) + (orden+1) * sizeof(unsigned int);
	char* bufferMeta = new char[tamanioMetadata];
	//leo el archivo
	fread(bufferMeta,tamanioMetadata,1,file);

	//leo el tamanio del archivo original.
	longitud = sizeof(unsigned int);
	memcpy(&tamanioOriginal,&bufferMeta[actual],longitud);
	//Obtengo la longitud del archivo comprimido
	unsigned int aux = ftell(file);
	fseek(file,0,SEEK_END);
	tamanioComprimido = ftell(file);
	fseek(file,aux,SEEK_CUR);

	tamanioMetadataTotal = strlen(nombreArchivo) + tamanioMetadata + sizeof(unsigned int) + sizeof(unsigned int);
	std::cout << "Estadisticas del archivo " << archivo_origen << "." << std::endl;
	std::cout <<"Orden de compresion:: " << orden << "." << std::endl;
	std::cout <<"Nombre original: " << nombreArchivo << "." << std::endl;
	std::cout <<"Tamanio original: "<< tamanioOriginal << " bytes." << endl;
	std::cout <<"Tamanio metadata: "<< tamanioMetadataTotal  << " bytes." << endl;
	std::cout <<"Tamanio total comprimido: "<< tamanioComprimido << " bytes." << endl;

	//imprimo aciertos de orden
	for (unsigned int index = 0; index <= orden; index++) {

		actual += sizeof(unsigned int);
		memcpy(&cantAciertos,&bufferMeta[actual],longitud);
		std::cout<<"-Orden:"<<index<<"  Aciertos: "<<cantAciertos<<endl;
	}

	//libero recursos utilizados
	fclose(file);
	delete[] bufferMeta;
	delete[] nombreArchivo;

	return Compresor::OPERACION_EXITOSA;
}

//FUNCIONES PRIVADAS
int Compresor::abrir_archivos_comprimir(string nombre_archivo,FILE ** file_origen,FILE** file_destino){

	std::cout<<endl<<"- Archivo a comprimir: "<<nombre_archivo;
	std::cout<<endl<<"- Archivo destino compreso: "<<get_nombre_archivo_comprimido(nombre_archivo)<<endl;

	//abro el archivo origen
	*file_origen = fopen(nombre_archivo.c_str(),"r");
	//si no puedo abrir el archivo se termina la apertura con fracaso
	if(!*file_origen) return 1;

	//abro el archivo en modo binario
	*file_destino = fopen (get_nombre_archivo_comprimido(nombre_archivo).c_str(),"w+");
	//si no puedo abrir el archivo cierro el archivo origen y retorno 1(fracaso)
	if(*file_destino == NULL){
		fclose(*file_origen);
		return 1;
	}

	return 0;
}

int Compresor::abrir_archivos_descomprimir(string nombre_archivo,FILE** file_origen,FILE** file_destino,unsigned int * orden, char**nombreArchivoOriginal){

	std::cout<<endl<<"- Archivo origen ya comprimido: "<<nombre_archivo;

	*file_origen = fopen(nombre_archivo.c_str(), "r");
	if (*file_origen == NULL)
		return 1;

	obtenerInformacionBasica(*file_origen,orden,nombreArchivoOriginal);

	if (*orden >= 0 || *orden <= 100) {
		std::cout<<endl<<"- Archivo destino descomprimido: "<< *nombreArchivoOriginal <<endl;

		*file_destino = fopen(*nombreArchivoOriginal, "w");
		if (*file_destino == NULL){
			fclose(*file_origen);
			return 1;
		}

		return 0;
	} else {
		return 1;
	}
}

string Compresor::get_nombre_archivo_comprimido(string archivo_origen){

	//agrego la extension .klow
	archivo_origen += ".klow";
	return archivo_origen;
}

string Compresor::get_nombre_original(string archivo_origen){

	return archivo_origen.substr(0,archivo_origen.length()-5);
}

Contexto* Compresor::crear_contexto_copia(Contexto* contexto_actual){

	//creo el nuevo contexto
	Contexto* nuevo_contexto = new Contexto(contexto_actual->getNombre());
	//agrego los caracteres al
	TTablaFreq* tabla = contexto_actual->getTabla();

	for(mapIt it = tabla->begin(); it!= tabla->end();it++){

		if(!exclusion.estaExcluido(it->first)){
			std::pair<char,unsigned int> par;
			par.first = it->first;
			par.second= it->second;
			nuevo_contexto->agregarCaracter(par);
		}
	}
	nuevo_contexto->setEsc(contexto_actual->getCantidadDeEscapes());

	return nuevo_contexto;
}

void Compresor::excluir_caracteres(Contexto* contexto){

	TTablaFreq* tabla = contexto->getTabla();

	for(mapIt it= tabla->begin(); it!= tabla->end();it++){
		exclusion.excluir(it->first);
	}
}

void Compresor::persistir_metadata(FILE* file,unsigned int orden, const char * nombre_archivo, unsigned int tamanioOriginal){
	/**
	 * FORMATO DEL METADATA
	 *
	 * [orden:int] [longiud del nombre de archivo original] [nombre original]
	 * [longitud del archivo inicial: long int] [frecc orden0: int] ... [frecc orden N: int]
	 *
	 */
	//primeros datos a guardar
	int orden_compresor = orden;

	//creo el bloque para grabar en el archivo
	//el tamanio del bloque sera la suma de los tamanios de la longitud del archivo
	//mas el del orden mas el de los contextos
	int tamanio_bloque = sizeof(unsigned int) + sizeof(unsigned int) + strlen(nombre_archivo) +
							sizeof(unsigned long int) + (orden + 1) * sizeof(unsigned int);

	char* bloque = new char[tamanio_bloque];

	//variables auxiliares para grabar
	unsigned int longitud;
	unsigned int actual=0;
	unsigned int longitudNombre = strlen(nombre_archivo);

	//guardo el orden del metodo al bloque
	actual = 0;
	longitud = sizeof(int);
	memcpy(&bloque[actual],&orden_compresor,longitud);

	//guardo la longitud del nombre original
	actual+=longitud;
	longitud = sizeof(unsigned int);
	memcpy(&bloque[actual],&longitudNombre,longitud);

	//guardo el nombre
	actual+=longitud;
	longitud = longitudNombre;
	memcpy(&bloque[actual],nombre_archivo,longitud);

	//guardo la longitud del archivo original
	actual+=longitud;
	longitud = sizeof(unsigned int);
	memcpy(&bloque[actual],&tamanioOriginal,longitud);

	//guardo todos los contextos
	for(unsigned int cont = 0; cont <= orden ; cont++){

		actual+=longitud;
		int frecc_contexto = estadisticas[cont];
		longitud = sizeof(int);
		memcpy(&bloque[actual],&frecc_contexto,longitud);
	}
	actual+=longitud;
	// Me posiciono en el comienzo del archivo
	fseek(file,0,SEEK_SET);
	//escribo el bloque
	fwrite(bloque,actual,1,file);
	//
	delete []bloque;
}

void Compresor::inicializar_metada(FILE* file,unsigned int orden, const char * nombre_archivo) {
	//Se deja en blanco el espacio para la metadata
	//para completarlo luego de haber realizado la compresion
	//Tamanio orden, longitud datos, nombreoriginal, longitud archvo inicial, estadisticas
	unsigned int tamanio = sizeof(unsigned int) + sizeof(unsigned int) + strlen(nombre_archivo) +
							sizeof(unsigned long int) + (orden+1) * sizeof(unsigned int);
	fseek(file,tamanio,SEEK_SET);
}

void Compresor::obtenerInformacionBasica(FILE * file,unsigned int * orden, char ** nombre_archivo) {
	unsigned int longitudNombre;
	unsigned tamLeer = sizeof(unsigned int) + sizeof(unsigned int);
	char * miniBloque = new char [tamLeer];

	//Voy al inicio del archivo
	fseek(file,0,SEEK_SET);
	fread(miniBloque,tamLeer,1,file);

	memcpy(orden,miniBloque, sizeof(unsigned int));
	memcpy(&longitudNombre,&miniBloque[4], sizeof(unsigned int));

	*nombre_archivo = new char [longitudNombre+1];
	fread(*nombre_archivo,longitudNombre,1,file);
	(*nombre_archivo)[longitudNombre]='\0';

	delete[]miniBloque;
}
